Skip to content

KiteBot Railway one-click deploy (Phase 3) — IaC topology + deploy docs - #7

Merged
jerelvelarde merged 12 commits into
mainfrom
jerel/kitebot-railway-oneclick
Jul 17, 2026
Merged

KiteBot Railway one-click deploy (Phase 3) — IaC topology + deploy docs#7
jerelvelarde merged 12 commits into
mainfrom
jerel/kitebot-railway-oneclick

Conversation

@jerelvelarde

Copy link
Copy Markdown
Collaborator

What this adds

A Railway one-click deploy for the full KiteBot stack (Phase 3, builds on the merged Phases 1 & 2 in #6). The whole stack deploys as three services wired over Railway private networking, all from this one repo:

Service What Build
agent Python deep-research backend (agent/) nixpacks, uvicorn, /health (root agent/)
notion-mcp Notion MCP sidecar (pnpm notion-mcp) Node
channel KiteBot channel host (pnpm channel) Node

Deliverables

  • .railway/railway.ts — Infrastructure-as-Code topology (railway SDK v3.5.7): 3 services, reference-variable wiring (AGENT_URL, NOTION_MCP_URL), pinned ports, :: binds for IPv6 private networking, secrets via preserve(), per-service restart policy. Applied with railway config apply.
  • README "Deploy to Railway" — the IaC deploy flow, per-service secrets checklist, the Deploy-button publish steps, and a Notion cold-start note.
  • Dependency + launcher changes the deploy requires: tsx/dotenv/@notionhq/notion-mcp-server moved to dependencies (runtime-required by the deployed services); scripts/start-notion-mcp.ts gains a NOTION_MCP_HOST arg so the sidecar binds :: on Railway.

Notably, there is deliberately no agent/railway.toml — Railway forbids a service being managed by both IaC and config-as-code, so the agent's build+deploy config lives only in the IaC.

Review

Ran a full 7-agent unbiased CR to convergence (6 fix rounds + confirmations). The deploy files (.railway/railway.ts, scripts/start-notion-mcp.ts) are clean. The loop fixed: the notion-mcp port mismatch, dual IaC/config-as-code management, IPv6 binds, runtime-dep misclassification, a stale Notion-Version pin, a local-macOS bind regression, CORS edge cases, and supervisor exit/signal semantics.

Deferred to a follow-up (out of scope here)

The CR also surfaced pre-existing Phase-1/2 agent-backend issues (unrelated to the deploy) — most importantly a recursion_limit: 100 that's silently ignored on the AG-UI streaming path, plus research() error-handling and missing test coverage — tracked as a separate follow-up.

Deploy-time verification (needs a live Railway deploy — the deployer's step)

  • Confirm builder: "NIXPACKS" resolves the uv/Python agent (Railway's default is now Railpack; Dockerfile fallback is the hedge).
  • Confirm Railway's SIGTERM reaches the pnpm-launched services.

Test plan

pnpm check-types, pnpm test (108 passing), agent pytest (9 passing), and the IaC type-check all green. Live Slack + Railway deploy remain manual (need creds).

🤖 Generated with Claude Code

jerelvelarde and others added 12 commits July 16, 2026 10:28
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on plan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, runtime deps

- notion-mcp: pin NOTION_MCP_PORT=3001; agent dials ${{notion-mcp.NOTION_MCP_PORT}}
  (the sidecar binds NOTION_MCP_PORT, never Railway's $PORT — previous
  ${{notion-mcp.PORT}} pointed at a port nothing listened on).
- agent: fold agent/railway.toml into the IaC build/deploy config and delete the
  toml — Railway forbids a service being managed by both IaC and config-as-code.
  Carries builder=NIXPACKS, healthcheckTimeout=300, restartPolicy ON_FAILURE.
- agent: pin PORT=8123 so uvicorn's bind and ${{agent.PORT}} always agree.
- agent: bind uvicorn --host :: for Railway dual-stack/IPv6 private networking.
- deps: move tsx + dotenv to dependencies (runtime-required by the channel and
  notion-mcp services; would be pruned from a production-only install).
- docs: correct spec's notion-mcp port claim and INTELLIGENCE_CHANNEL_NAME
  classification; note OPENAI_MODEL default in README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, doc sync

- sidecar: scripts/start-notion-mcp.ts gains NOTION_MCP_HOST (validated, default
  127.0.0.1); IaC sets it to :: so the notion-mcp server (upstream default
  127.0.0.1) is reachable over Railway's IPv6 private network. Without this the
  agent's cross-container Notion connection was refused even with the port fixed.
- OPENAI_MODEL: stop managing it in the IaC (the agent defaults to gpt-5.5) so a
  UI override survives 'railway config apply' — was a plain value that a re-apply
  would silently clobber, contradicting the README's 'override in Variables' note.
- NOTION_MCP_AUTH_TOKEN: agent references ${{notion-mcp.NOTION_MCP_AUTH_TOKEN}}
  (single source of truth) instead of a second hand-matched secret.
- notion-mcp: add ON_FAILURE restart policy; document that it requires its two
  tokens and how to omit the service if you don't want Notion.
- deps: move @notionhq/notion-mcp-server to dependencies (runtime-required by the
  deployed notion-mcp service; npx -y would otherwise fetch latest at cold start).
- docs: sync spec + plan to the shipped implementation (no agent/railway.toml,
  IPv6 bind, port pinning, model unmanaged); fix README PORT/SERVER_PORT order,
  Tavily-vs-Responses web-search note, and channels-intelligence optionality.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on, invariant hardening

scripts/start-notion-mcp.ts:
- Stop injecting a global Notion-Version header: @notionhq/notion-mcp-server
  >=2.4 sources the API version per-operation from its OpenAPI spec, and a
  globally-configured Notion-Version OVERRIDES those defaults (verified in the
  installed 2.4.1 http-client.buildDefaultHeaders), pinning every call to a
  3-year-old version and breaking newer endpoints. NOTION_VERSION still forces
  one if needed.
- Exit handler distinguishes a forwarded shutdown (SIGINT/SIGTERM → exit 0) from
  an abnormal signal death (→ exit 1), so a clean stop no longer trips the
  notion-mcp ON_FAILURE restart / reads as a crash in logs.
- Add a SIGKILL escalation timeout on shutdown (child may ignore the signal, or
  shell:true may not propagate it).
- Tighten NOTION_MCP_HOST validation to reject a leading '-' (flag-injection shape).
- Correct the header docblock to describe the actual inbound/outbound token mapping.

.railway/railway.ts:
- Leave INTELLIGENCE_CHANNEL_NAME unmanaged (app/managed.ts defaults it to
  'kitebot') so a UI override survives config apply — same pattern as OPENAI_MODEL.
- Fix model-default citation to agent/agent.py.

agent/main.py: default SERVER_HOST to :: so the pnpm-agent/python-main path also
honors the IPv6 private-networking invariant (dual-stack; local still works).

docs: sync spec/plan to the shipped file (resources order, channel-name note).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/comments

scripts/start-notion-mcp.ts:
- Exit handler: check shuttingDown BEFORE the exit code, so a forwarded-signal
  shutdown that shell:true surfaces as a numeric 128+signum code (e.g. 143) still
  reports success and doesn't trip the notion-mcp ON_FAILURE restart policy.
- Treat an empty NOTION_MCP_PORT/NOTION_MCP_HOST ('' from a bare .env line) as
  unset → default, matching agent/main.py; clearer non-numeric port message.
- Correct the NOTION_TOKEN-deletion comment: OPENAPI_MCP_HEADERS takes precedence
  (server checks it before NOTION_TOKEN), so the delete is defensive cleanup.
- Note both NOTION_MCP_URL consumers (runtime.ts and the Python agent).

package.json: raise @notionhq/notion-mcp-server floor to ^2.4.0 — the launcher
relies on >=2.4 per-operation Notion-Version sourcing; ^2.2.1 allowed sub-2.4.

agent/main.py: make CORS origins env-driven (CORS_ALLOW_ORIGINS, default '*') so a
public exposure can be locked down; clarify the local-dev-only PORT block comment.

docs: README adds LINEAR_API_KEY to the .env example and clarifies the deploy flow
(pnpm install first so the CLI can evaluate the config; railway init to create);
spec drops the stale agent NOTION_MCP_AUTH_TOKEN secret and the OPENAI_MODEL/
INTELLIGENCE_CHANNEL_NAME wiring lines (now unmanaged); plan deploy block synced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ups, macOS bind

scripts/start-notion-mcp.ts:
- Spawn with shell only on Windows (npx.cmd/CVE-2024-27980); on POSIX spawn npx
  directly + detached, so the server runs in its own process group. Shutdown now
  signals the whole group (process.kill(-pid)) with a SIGKILL escalation, which
  reaps the underlying node server instead of just the shell wrapper and also
  removes the terminal-Ctrl-C race (the detached child no longer receives the
  terminal's SIGINT directly).
- Exit handler: any termination we did NOT initiate is a failure — exit non-zero
  even on code 0 (e.g. server exits after a bad flag) so Railway's ON_FAILURE
  restart fires and it's visible, instead of a silent code-0 passthrough.
- Header docstring names both consumers (runtime.ts + Python agent; Railway's
  sole consumer is the Python agent).

agent/main.py:
- Revert local SERVER_HOST default to 0.0.0.0 — a :: default risked local macOS
  dev (a :: socket may not accept 127.0.0.1). Railway still binds :: via the
  startCommand; this __main__ path is local-only.
- CORS: treat an empty/blank CORS_ALLOW_ORIGINS as unset -> '*' (an empty
  allowlist would otherwise block every origin, contradicting the default).

docs: README documents the Notion cold-start race + redeploy workaround; spec
fixes the INTELLIGENCE_CHANNEL_NAME self-contradiction (it is NOT set in the IaC);
package.json dependency ordering tidied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rce warnings, parity

agent/main.py:
- CORS: fall back to ['*'] when the resolved allowlist is empty, so a
  whitespace/comma-only CORS_ALLOW_ORIGINS no longer blocks every origin.
- SERVER_HOST / SERVER_PORT: treat set-but-empty as unset (`getenv(x) or default`),
  matching the convention already used for PORT/CORS.
- Route [ERROR] diagnostics to stderr.

agent/tools.py: when an internal source is CONFIGURED (token set) but its MCP
server times out / is unavailable, warn loudly on stderr that the agent is
running WITHOUT those tools — distinguishing a real degradation (e.g. the Railway
cold-start race) from a source the operator intentionally left disabled.

.railway/railway.ts: give the channel service the same ON_FAILURE restart policy
as agent/notion-mcp (parity — don't leave KiteBot silently offline on a crash).

scripts/start-notion-mcp.ts: correct the validation comments (`shell: isWindows`,
not an unconditional `shell: true`).

docs: reconcile the spec's deploy commands with the README (railway init to
create a project, or railway link for an existing one).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…an channel restart-policy back-port

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jerelvelarde
jerelvelarde merged commit d05d5a8 into main Jul 17, 2026
jerelvelarde added a commit that referenced this pull request Jul 27, 2026
`.migration-discovery.md` was a scratch artifact sitting at the repo root,
ungitignored, and contradicted by the PR that carried it: it records the
runtime floor staying at ^1.62.3 and channels-intelligence as a direct dep,
while the shipped branch raises the floor to ^1.63.2 and drops that dep.

Its 571 lines of verified SDK signatures are worth keeping, so move it beside
the design doc and plan under docs/superpowers/specs/ (the convention PRs #6
and #7 established) and head it with a note marking it a point-in-time record
plus the two places the implementation deliberately diverged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant